package edu.ptu.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import edu.ptu.shortlink.project.common.convention.exception.ClientException;
import edu.ptu.shortlink.project.common.convention.exception.ServiceException;
import edu.ptu.shortlink.project.dao.entity.*;
import edu.ptu.shortlink.project.dao.mapper.*;
import edu.ptu.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import edu.ptu.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import edu.ptu.shortlink.project.dto.req.ShortLinkPageReqDTO;
import edu.ptu.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import edu.ptu.shortlink.project.dto.resp.*;
import edu.ptu.shortlink.project.service.IShortlinkService;
import edu.ptu.shortlink.project.toolkit.HashUtil;
import edu.ptu.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static edu.ptu.shortlink.project.common.constant.RedisKeyConstant.*;
import static edu.ptu.shortlink.project.common.constant.ShortLinkAccessStatsConstants.*;
import static edu.ptu.shortlink.project.common.enums.VailDateTimeEnums.PERMANENT;

/**
 * @BelongsProject: shortlink
 * @BelongsPackage: edu.ptu.shortlink.project.service.impl
 * @Author: HYJ
 * @CreateTime: 2025-04-26  21:09
 * @Description: 短链接业务实现层
 * @Version: 1.0
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortlinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements IShortlinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;

    //高德地图key
    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocalAmapKey;

    //默认域名
    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {

        //生成短链接
        String shortLinkSuffix = generateSuffix(requestParam);

        String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain).append("/").append(shortLinkSuffix).toString();

        ShortLinkDO shortLinkDO = BeanUtil.toBean(requestParam, ShortLinkDO.class);
        //修改参数
        shortLinkDO
                .setFullShortUrl(fullShortUrl)
                .setDomain(createShortLinkDefaultDomain)
                .setGid(requestParam.getGid())
                .setOriginUrl(requestParam.getOriginUrl())
                .setShortUri(shortLinkSuffix)
                .setValidDateType(requestParam.getValidDateType())
                .setValidDate(requestParam.getValidDate())
                .setDelTime(0L)
                .setTotalPv(0)
                .setTotalUv(0)
                .setTotalUip(0)
                .setCreateType(requestParam.getCreateType())
                .setDescribe(requestParam.getDescribe())
                .setFavicon(getFavicon(requestParam.getOriginUrl()))
                .setEnableStatus(0);

        // 插入路由表
        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                .gid(requestParam.getGid())
                .fullShortUrl(fullShortUrl)
                .build();

        //对布隆过滤器过滤器误判进行保障
        try {
            baseMapper.insert(shortLinkDO);
            // 插入路由表
            shortLinkGotoMapper.insert(shortLinkGotoDO);
        } catch (DuplicateKeyException ex) {
            //对误判的短链接进行处理
            //1.存在在缓存中
            //2.不存在在缓存中
            log.warn("短链接 {} 重复入库", fullShortUrl);
            throw new ServiceException(String.format("短链接:%s,生成重复", fullShortUrl));
        }

        //缓存预热
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                shortLinkDO.getOriginUrl(),
                LinkUtil.getShortLintCacheValidDate(requestParam.getValidDate()),
                TimeUnit.MILLISECONDS);

        //添加至布隆
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);

        //返回实体
        return ShortLinkCreateRespDTO.
                builder()
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();

    }

    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {

        //获取参数
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();

        ArrayList<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();

        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);

                ShortLinkBaseInfoRespDTO shortLinkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .build();
                result.add(shortLinkBaseInfoRespDTO);
            } catch (Exception e) {
                log.error("批量创建短链接失败,原始参数: {}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .baseLinkInfos(result)
                .total(result.size())
                .build();
    }


    //生成短链接->引入布隆过滤器快速判断避免对数据库的缓存穿透
    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {

        int maxRetries = 0;
        String shortUri;

        while (true) {
            if (maxRetries > 10) {
                throw new ServiceException("访问繁忙,请稍后再试");
            }

            //加上随机数防止短链接重复
            String originUrl = requestParam.getOriginUrl();
            originUrl += UUID.randomUUID().toString();

            shortUri = HashUtil.hashToBase62(originUrl);
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shortUri)) {
                break;
            }
            maxRetries++;
        }

        return shortUri;
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {

        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);

        return resultPage.convert(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://" + each.getDomain());
            return result;
        });
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> countShortLink(List<String> gIds) {

        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO()).select("gid", "count(*) as shortLinkCount").in("gid", gIds).eq("enable_status", 0).groupBy("gid");

        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDOList, ShortLinkGroupCountQueryRespDTO.class);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        // 校验短链接是否存在
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid, requestParam.getGid()).eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl()).eq(ShortLinkDO::getEnableStatus, 0);

        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接不存在");
        }

        // 不修改分组，则直接更新
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) {
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), PERMANENT.getType()), ShortLinkDO::getValidDateType, null);
            //存在则修改
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .favicon(hasShortLinkDO.getFavicon()).
                    clickNum(hasShortLinkDO.getClickNum())
                    .createType(hasShortLinkDO.getCreateType())
                    .originUrl(requestParam.getOriginUrl())
                    .validDate(requestParam.getValidDate())
                    .delTime(0L)
                    .validDateType(requestParam.getValidDateType())
                    .gid(requestParam.getGid())
                    .describe(requestParam.getDescribe())
                    .build();
            baseMapper.update(shortLinkDO, updateWrapper);
        } else {
            //使用写锁
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));

            //获取锁
            RLock writeLock = readWriteLock.writeLock();
            writeLock.lock();
            try {

                // 需要修改分组，则删除旧的短链接并插入新的短链接
                LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0)
                        .set(ShortLinkDO::getDelTime, System.currentTimeMillis());
                baseMapper.delete(updateWrapper);

                //插入新的短链接
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .shortUri(hasShortLinkDO.getShortUri())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .favicon(Objects.equals(requestParam.getOriginUrl(), hasShortLinkDO.getOriginUrl()) ? hasShortLinkDO.getFavicon() : getFavicon(requestParam.getOriginUrl()))
                        .enableStatus(0)
                        .createType(hasShortLinkDO.getCreateType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .build();
                baseMapper.insert(shortLinkDO);

                //删除旧的路由表
                LambdaUpdateWrapper<ShortLinkGotoDO> linkGotoUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                shortLinkGotoMapper.delete(linkGotoUpdateWrapper);

                //插入新的路由表
                ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                        .fullShortUrl(requestParam.getFullShortUrl())
                        .gid(requestParam.getGid())
                        .build();
                shortLinkGotoMapper.insert(shortLinkGotoDO);
            } finally {
                writeLock.unlock();
            }

            //对日期变更进行处理
            if (!Objects.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType())
                    || !Objects.equals(hasShortLinkDO.getValidDate(), requestParam.getValidDate())
                    || !Objects.equals(hasShortLinkDO.getOriginUrl(), requestParam.getOriginUrl()))//变更跳转链接
            {
                //删除跳转键
                stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));

                if (hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(new Date())) {//若是将过期短链恢复
                    if (Objects.equals(requestParam.getValidDateType(), PERMANENT.getType()) ||//恢复成永久有效的
                            requestParam.getValidDate().after(new Date())) {//恢复成有效期之内的
                        //删除判空键
                        stringRedisTemplate.delete(String.format(SHORT_LINK_IS_NULL_KEY, requestParam.getFullShortUrl()));
                    }
                }
            }
        }
    }


    @Override
    public void redirectUrl(String shortUri, HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 获取域名+短链接标识=>完整短链接
        String domain = req.getServerName();

        //获取端口号
        String serverPort = Optional.of(req.getServerPort())
                .filter(port -> !Objects.equals(port, 80))
                .map(String::valueOf)
                .map(port -> ":" + port)
                .orElse("");

        String fullShortUrl = domain + serverPort + "/" + shortUri;

        // 从缓存中查询是否存在
        String orginalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));

        //如果存在则进行跳转
        if (StrUtil.isNotBlank(orginalLink)) {
            // 访问统计
            shortLinkAccessStats(fullShortUrl, null, req, resp);
            resp.sendRedirect(orginalLink);
            return;
        }

        // 从布隆过滤器中查询是否存在
        if (!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
            // 不存在返回
            resp.sendRedirect("/page/notfound");
            return;
        }

        // 从redis中查询是否存在
        String shortLinkIsNull = stringRedisTemplate.opsForValue().get(String.format(SHORT_LINK_IS_NULL_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(shortLinkIsNull)) {
            //不为空则表明短链接不存在
            resp.sendRedirect("/page/notfound");
            return;
        }


        RLock lock = redissonClient.getLock(String.format(LOCK_SHORT_LINK_GOTO_KEY, fullShortUrl));
        // 加锁
        lock.lock();
        try {
            // 再次查询缓存
            String originalUrl = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            // 如果缓存中不存在，则查询数据库
            if (StrUtil.isBlank(originalUrl)) {

                //查询goto表
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapperqueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);

                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapperqueryWrapper);

                if (shortLinkGotoDO == null) {
                    //进行风控解决缓存穿透问题->设为空值
                    stringRedisTemplate.opsForValue().set(String.format(SHORT_LINK_IS_NULL_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                    resp.sendRedirect("/page/notfound");
                    return;
                }

                //查询短链接表获取originUrl
                LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                        .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                        .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                        .eq(ShortLinkDO::getEnableStatus, 0);

                ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);

                //数据库中不存在或者短链接已经过期则跳转404页面
                if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))) {
                    //短链接已经过期
                    stringRedisTemplate.opsForValue().set(String.format(SHORT_LINK_IS_NULL_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                    resp.sendRedirect("/page/notfound");
                    return;
                }

                // 缓存原始链接
                stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl());

                //预热缓存
                stringRedisTemplate.opsForValue().set(
                        String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                        shortLinkDO.getOriginUrl(),
                        LinkUtil.getShortLintCacheValidDate(shortLinkDO.getValidDate()),
                        TimeUnit.MILLISECONDS);

                // 访问统计
                shortLinkAccessStats(fullShortUrl, shortLinkDO.getGid(), req, resp);
                // 跳转
                resp.sendRedirect(shortLinkDO.getOriginUrl());
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * 短链接访问统计
     */
    @Transactional(rollbackFor = Exception.class)
    private void shortLinkAccessStats(String fullShortUrl, String gid, HttpServletRequest req, HttpServletResponse resp) {

        //判断是否是第一次访问
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = req.getCookies();

        AtomicReference<String> uv = new AtomicReference<>();

        Runnable addRespCookie = () -> {
            //使用cookie进行uv统计
            uv.set(UUID.fastUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);

            // 设置cookie的路径为当前请求的路径
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            resp.addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);

            //使用set数据结构判断是否已经存在
            stringRedisTemplate.opsForSet().add(String.format(SHORT_LINK_UV_STATS_KEY, fullShortUrl), uv.get());
        };

        // 判断uv是否是第一次访问
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {

                        uv.set(each);
                        //判断是否存在
                        Long uvAdded = stringRedisTemplate.opsForSet().add(String.format(SHORT_LINK_UV_STATS_KEY, fullShortUrl), each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addRespCookie);
        } else {
            addRespCookie.run();
        }

        //uip访问统计
        String remoteAddr = LinkUtil.getActualIp(req);
        Long uipAdded = stringRedisTemplate.opsForSet().add(String.format(SHORT_LINK_UIP_STATS_KEY, fullShortUrl), remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;


        if (StrUtil.isBlank(gid)) {
            //查询gid
            LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapperqueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);

            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapperqueryWrapper);
            gid = shortLinkGotoDO.getGid();
        }

        // 访问统计
        int hour = DateUtil.hour(new Date(), true);
        int weekday = DateUtil.dayOfWeekEnum(new Date()).getIso8601Value();

        LinkAccessStatsDO linkAccessStats = LinkAccessStatsDO.builder()
                .pv(1)
                .uv(uvFirstFlag.get() ? 1 : 0)
                .uip(uipFirstFlag ? 1 : 0)
                .hour(hour)
                .weekday(weekday)
                .date(new Date())
                .fullShortUrl(fullShortUrl)
                .build();
        linkAccessStatsMapper.shortLinkAccessStatsInc(linkAccessStats);

        //统计地区信息
        HashMap<String, Object> localeParam = new HashMap<>();
        localeParam.put("key", statsLocalAmapKey);
        localeParam.put("ip", remoteAddr);

        String localeReqUrl = HttpUtil.get(AMAP_IP_URL, localeParam);
        JSONObject localeResultObj = JSON.parseObject(localeReqUrl);


        //判断是否成功
        String infocode = localeResultObj.getString("infocode");

        String actualProvince = "";
        String actualCity = "";

        if (StrUtil.isNotBlank(infocode) && StrUtil.equals(infocode, "10000")) {

            String province = localeResultObj.getString("province");
            boolean unknowFlag = StrUtil.equals(province, "[]");

            LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .province(actualProvince = (unknowFlag ? "未知" : province))
                    .city(actualCity = (unknowFlag ? "未知" : localeResultObj.getString("city")))
                    .adcode(unknowFlag ? "未知" : localeResultObj.getString("adcode"))
                    .country("中国")
                    .build();
            linkLocaleStatsMapper.shortLinkLocaleStatsInc(linkLocaleStatsDO);
        }

        //操作系统统计
        String os = LinkUtil.getOs(req);
        LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                .cnt(1)
                .fullShortUrl(fullShortUrl)
                .date(new Date())
                .os(os)
                .build();
        linkOsStatsMapper.shortLinkOsStatsInc(linkOsStatsDO);

        //浏览器统计
        String browser = LinkUtil.getBrowser(req);
        LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                .cnt(1)
                .fullShortUrl(fullShortUrl)
                .date(new Date())
                .browser(browser)
                .build();
        linkBrowserStatsMapper.shortLinkBrowserStatsInc(linkBrowserStatsDO);

        //设备统计
        String device = LinkUtil.getDevice(req);
        LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                .cnt(1)
                .fullShortUrl(fullShortUrl)
                .date(new Date())
                .device(device)
                .build();
        linkDeviceStatsMapper.shortLinkDeviceStatsInc(linkDeviceStatsDO);

        //网络统计
        String network = LinkUtil.getNetwork(req);
        LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                .cnt(1)
                .fullShortUrl(fullShortUrl)
                .date(new Date())
                .network(network)
                .build();
        linkNetworkStatsMapper.shortLinkNetworkStatsInc(linkNetworkStatsDO);

        //日志访问统计
        LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                .fullShortUrl(fullShortUrl)
                .browser(browser)
                .ip(remoteAddr)
                .user(uv.get())
                .network(network)
                .device(device)
                .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
                .build();
        linkAccessLogsMapper.insert(linkAccessLogsDO);

        // 访问量+1
        baseMapper.increase(
                gid,
                fullShortUrl,
                1,
                uvFirstFlag.get() ? 1 : 0,
                uipFirstFlag ? 1 : 0);
        //今日访问量统计
        LinkStatsTodayDO statsTodayDO = LinkStatsTodayDO.builder()
                .gid(gid)
                .fullShortUrl(fullShortUrl)
                .date(new Date())
                .todayPv(1)
                .todayUv(uvFirstFlag.get() ? 1 : 0)
                .todayUip(uipFirstFlag ? 1 : 0)
                .build();
        linkStatsTodayMapper.shortLinkStatsToday(statsTodayDO);

    }


    /**
     * 获取url标题
     */
    @SneakyThrows
    private String getFavicon(String originUrl) {
        URL targetUrl = new URL(originUrl);

        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();

        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(originUrl).get();

            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }
}
